import datetime
import logging
import subprocess
import time
import os
import re
import shutil
from threading import Event

import yaml

from yandextank.contrib.netort.netort.resource import TempDownloaderOpenerProtocol

from .reader import PandoraStatsReader
from ..Console import Plugin as ConsolePlugin
from ..Console import screen as ConsoleScreen
from ..Phantom import PhantomReader, string_to_df
from ...common.interfaces import AbstractInfoWidget, GeneratorPlugin
from ...common.util import tail_lines, FileMultiReader

logger = logging.getLogger(__name__)


class Plugin(GeneratorPlugin):
    """Pandora load generator plugin"""

    OPTION_CONFIG = "config"
    SECTION = "pandora"
    DEFAULT_REPORT_FILE = "phout.log"
    DEFAULT_EXPVAR_PORT = 1234

    def __init__(self, core, cfg, name):
        super(Plugin, self).__init__(core, cfg, name)
        self.output_finished = Event()
        self.enum_ammo = False
        self.pandora_cmd = None
        self.pandora_config_file = None
        self.config_contents = None
        self.custom_config = False
        self.expvar = self.get_option('expvar')
        self.expvar_enabled = bool(self.expvar)
        self.expvar_port = self.DEFAULT_EXPVAR_PORT
        self.report_files = None
        self.__address = None
        self.__schedule = None
        self.ammofile = None
        self.process_stderr_file = None
        self.resources = []

    @staticmethod
    def get_key():
        return __file__

    def get_available_options(self):
        opts = ["pandora_cmd", "buffered_seconds", "config_content", "config_file"]
        return opts

    def configure(self):
        self.report_files = [self.get_option("report_file")]
        self.buffered_seconds = self.get_option("buffered_seconds")
        self.affinity = self.get_option("affinity", "")
        self.resources = self.get_option("resources")

    def prepare_resources(self):
        # if we use custom pandora binary, we can download it and make it executable
        self.pandora_cmd = self.get_resource(self.get_option("pandora_cmd"), "./pandora", permissions=0o755)

        # download all resources from self.get_options("resources")
        if len(self.resources) > 0:
            for resource in self.resources:
                self.get_resource(resource["src"], resource["dst"])

        # get config_contents and patch it: expand resources via resource manager
        # config_content option has more priority over config_file
        if self.get_option("config_content"):
            logger.info('Found config_content option configuration')
            self.config_contents = self.__patch_raw_config_and_dump(self.get_option("config_content"))
        elif self.get_option("config_file"):
            logger.info('Found config_file option configuration')
            with open(self.get_option("config_file"), 'rb') as config:
                external_file_config_contents = yaml.load(config.read(), Loader=yaml.FullLoader)
            self.config_contents = self.__patch_raw_config_and_dump(external_file_config_contents)
        else:
            raise RuntimeError("Neither pandora.config_content, nor pandora.config_file specified")
        logger.debug('Config after parsing for patching: %s', self.config_contents)

        # find report filename and add to artifacts
        self.report_files = [pool['result']['destination'] for pool in self.config_contents['pools']]
        for f in self.report_files:
            with open(f, 'w'):
                pass
            self.core.add_artifact_file(f)

    def __patch_raw_config_and_dump(self, cfg_dict):
        if not cfg_dict:
            raise RuntimeError('Empty pandora config')
        # patch
        config_content = self.patch_config(cfg_dict)
        # dump
        self.pandora_config_file = self.core.mkstemp(".yaml", "pandora_config_")
        self.core.add_artifact_file(self.pandora_config_file)
        with open(self.pandora_config_file, 'w') as config_file:
            yaml.dump(config_content, config_file)
        return config_content

    def patch_config(self, config):
        """
        download remote resources, replace links with local filenames
        add result file section
        :param dict config: pandora config
        """
        # get expvar parameters
        if config.get("monitoring"):
            if isinstance(config["monitoring"].get("expvar"), dict):
                self.expvar_enabled = config["monitoring"]["expvar"].get("enabled")
                if config["monitoring"]["expvar"].get("port"):
                    self.expvar_port = config["monitoring"]["expvar"].get("port")
        # or set if expvar not exists
        elif not self.expvar:
            config["monitoring"] = {
                "expvar": {
                    "enabled": True,
                    "port": self.DEFAULT_EXPVAR_PORT,
                }
            }
            self.expvar_enabled = True

        # FIXME this is broken for custom ammo providers due to interface incompatibility
        # FIXME refactor pandora plx
        for n, pool in enumerate(config['pools']):
            if pool.get('ammo', {}).get('file', ''):
                self.ammofile = pool['ammo']['file']
                opener = self.core.resource_manager.get_opener(self.ammofile)
                if isinstance(opener, TempDownloaderOpenerProtocol):
                    pool['ammo']['file'] = opener.download_file(True, try_ungzip=True)
                else:
                    pool['ammo']['file'] = opener.filename

            if not pool.get('result') or 'phout' not in pool.get('result', {}).get('type', ''):
                logger.warning('Seems like pandora result file not specified... adding defaults')
                pool['result'] = dict(
                    destination=f"{n}_{self.DEFAULT_REPORT_FILE}",
                    type='phout',
                )

            if pool.get('gun').get('answlog'):
                answ = pool.get('gun').get('answlog')
                if answ.get('enabled') is True:
                    if not answ.get('path'):
                        answ_path = self.core.mkstemp(".log", "answ_")
                        logger.warning(
                            'Seems like pandora answer log file not specified... adding default path: %s', answ_path
                        )
                        self.core.add_artifact_file(answ_path)
                        config['pools'][n]['gun']['answlog']['path'] = answ_path
                    else:
                        answ_path = answ.get('path')
                        self.core.add_artifact_file(answ_path)

        return config

    @property
    def address(self):
        if not self.__address:
            for pool in self.config_contents['pools']:
                if pool.get('gun', {}).get('target'):
                    self.__address = pool.get('gun', {}).get('target')
                    break
            else:
                self.__address = 'unknown'
        return self.__address

    @property
    def schedule(self):
        if not self.__schedule:
            for pool in self.config_contents['pools']:
                if pool.get('rps'):
                    self.__schedule = pool.get('rps')
                    break
            else:
                self.__schedule = 'unknown'
        return self.__schedule

    def get_info(self):
        return self.Info(
            address=self.address,
            ammo_file=self.ammofile,
            duration=0,
            instances=0,
            loop_count=0,
            port=self.address.split(':')[-1],
            rps_schedule=self.schedule,
        )

    def get_reader(self, parser=string_to_df):
        if self.reader is None:
            self.reader = [FileMultiReader(f, self.output_finished) for f in self.report_files]
        return [PhantomReader(reader.get_file(), parser=parser) for reader in self.reader]

    def get_stats_reader(self):
        if self.stats_reader is None:
            self.stats_reader = PandoraStatsReader(self.expvar_enabled, self.expvar_port)
        return self.stats_reader

    def get_resource(self, resource, dst, permissions=0o644):
        opener = self.core.resource_manager.get_opener(resource)
        if isinstance(opener, TempDownloaderOpenerProtocol):
            tmp_path = opener.download_file(True, try_ungzip=True)
            shutil.copy(tmp_path, dst)
            logger.info('Successfully moved resource %s', dst)
        else:
            dst = opener.filename
        try:
            os.chmod(dst, permissions)
        except OSError:
            logger.warning('Cannot change permissions to %s', dst)
        return dst

    def prepare_test(self):
        self.prepare_resources()

        try:
            console = self.core.get_plugin_of_type(ConsolePlugin)
        except KeyError as ex:
            logger.debug(ex)
            console = None

        if console:
            widget = PandoraInfoWidget(self)
            console.add_info_widget(widget)
            self.core.job.aggregator.add_result_listener(widget)

    def start_test(self):
        args = [self.pandora_cmd] + (['-expvar'] if self.expvar else []) + [self.pandora_config_file]
        if self.affinity:
            self.core.__setup_affinity(self.affinity, args=args)
        logger.info("Starting: %s", args)
        self.start_time = time.time()
        self.process_stderr_file = self.core.mkstemp(".log", "pandora_")
        self.core.add_artifact_file(self.process_stderr_file)
        self.process_stderr = open(self.process_stderr_file, 'w')
        try:
            self.process = subprocess.Popen(
                args, stderr=self.process_stderr, stdout=self.process_stderr, close_fds=True
            )
        except OSError:
            logger.debug("Unable to start Pandora binary. Args: %s", args, exc_info=True)
            raise RuntimeError("Unable to start Pandora binary and/or file does not exist: %s" % args)

    def is_test_finished(self):
        retcode = self.process.poll()
        if retcode is not None and retcode == 0:
            logger.info("Pandora subprocess done its work successfully and finished w/ retcode 0")
            self.output_finished.set()
            return retcode
        elif retcode is not None and retcode != 0:
            lines_amount = 20
            logger.info("Pandora finished with retcode %s. Last %s logs of Pandora log:", retcode, lines_amount)
            self.output_finished.set()
            last_log_contents = tail_lines(self.process_stderr_file, lines_amount)
            for logline in last_log_contents:
                logger.info(logline.strip('\n'))
            with open(self.process_stderr_file) as stderr:
                for line in stderr:
                    if self.check_log_line_contains_error(line):
                        self.errors.append(line.strip())
            return abs(retcode)
        else:
            return -1

    @staticmethod
    def check_log_line_contains_error(line):
        return re.search('^panic:|ERROR|FATAL', line) is not None

    def end_test(self, retcode):
        if self.process and self.process.poll() is None:
            logger.warning("Terminating worker process with PID %s", self.process.pid)
            self.process.terminate()
            if self.process_stderr:
                self.process_stderr.close()
        else:
            logger.info("Seems Pandora subprocess finished")
        self.output_finished.set()
        return retcode


class PandoraInfoWidget(AbstractInfoWidget):
    '''Right panel widget'''

    def __init__(self, pandora):
        AbstractInfoWidget.__init__(self)
        self.krutilka = ConsoleScreen.krutilka()
        self.owner = pandora
        self.reqps = 0
        self.active = 0

    def get_index(self):
        return 0

    def on_aggregated_data(self, data, stats):
        self.reqps = stats["metrics"]["reqps"]
        self.active = stats["metrics"]["instances"]

    def render(self, screen):
        text = " Pandora Test %s" % next(self.krutilka)
        space = screen.right_panel_width - len(text) - 1
        left_spaces = space // 2
        right_spaces = space // 2

        dur_seconds = int(time.time()) - int(self.owner.start_time)
        duration = str(datetime.timedelta(seconds=dur_seconds))

        template = (
            screen.markup.BG_BROWN + '~' * left_spaces + text + ' ' + '~' * right_spaces + screen.markup.RESET + "\n"
        )
        template += "Command Line: %s\n"
        template += "    Duration: %s\n"
        template += "  Requests/s: %s\n"
        template += " Active reqs: %s\n"
        template += "      Target: %s\n"
        template += "    Schedule: \n%s\n"
        data = (
            self.owner.pandora_cmd,
            duration,
            self.reqps,
            self.active,
            self.owner.address,
            yaml.dump(self.owner.schedule),
        )

        return template % data
